package com.smart.milvus.autoconfigure;

import com.smart.milvus.bean.BaseMilvusMapper;
import com.smart.milvus.bean.MilvusModel;
import com.smart.milvus.util.ClassUtils;
import com.smart.milvus.util.StringUtils;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.scope.ScopedProxyFactoryBean;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.type.ClassMetadata;
import org.springframework.util.Assert;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;

@Slf4j
@Setter
public final class MilvusClientBeanScanner extends ClassPathBeanDefinitionScanner {

    private Boolean lazyInitialization;
    private String defaultScope;
    private Class<? extends MilvusClientFactoryBean> clientFactoryBeanClass = MilvusClientFactoryBean.class;

    public MilvusClientBeanScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }

    @Override
    protected void registerDefaultFilters() {
        this.addIncludeFilter((metadataReader, metadataReaderFactory) -> {
            ClassMetadata classMetadata = metadataReader.getClassMetadata();
            if (!classMetadata.isInterface()) {
                return false;
            }
            String className = classMetadata.getClassName();
            if (StringUtils.isBlank(className)) {
                return false;
            }
            Class<?> objectClass = null;
            try {
                objectClass = ClassUtils.loadClass(className);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
            return BaseMilvusMapper.class.isAssignableFrom(objectClass);
        });
        super.registerDefaultFilters();
    }

    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions.isEmpty()) {
            log.warn("No milvus mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
        } else {
            try {
                this.processBeanDefinitions(beanDefinitions);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
        return beanDefinitions;
    }

    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) throws ClassNotFoundException {
        BeanDefinitionRegistry registry = this.getRegistry();
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
        ApplicationContext applicationContext = (ApplicationContext) getResourceLoader();
        for (BeanDefinitionHolder holder : beanDefinitions) {
            AbstractBeanDefinition definition = (AbstractBeanDefinition) holder.getBeanDefinition();
            String beanClassName = definition.getBeanClassName();
            if (StringUtils.isBlank(beanClassName)) {
                registry.removeBeanDefinition(holder.getBeanName());
                continue;
            }
            Class<? extends MilvusModel> entityType;
            Class<?> beanClass = ClassUtils.loadClass(beanClassName);
            Type[] genericInterfaces = beanClass.getGenericInterfaces();
            if (genericInterfaces != null && genericInterfaces.length == 1 && (genericInterfaces[0] instanceof ParameterizedType)) {
                ParameterizedType parameterizedType = (ParameterizedType) genericInterfaces[0];
                entityType = (Class<? extends MilvusModel>) parameterizedType.getActualTypeArguments()[0];
            } else {
                throw new IllegalArgumentException("Unable to determine the entity type.");
            }

            AbstractBeanDefinition beanDefinition = new GenericBeanDefinition();
            boolean scopedProxy = false;
            if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
                beanDefinition = (AbstractBeanDefinition) Optional.ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition())
                        .map(BeanDefinitionHolder::getBeanDefinition)
                        .orElseThrow(() -> new IllegalStateException("The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]"));
                scopedProxy = true;
            }

            beanDefinition.setBeanClass(this.clientFactoryBeanClass);
            beanDefinition.getPropertyValues().add("applicationContext", applicationContext);
            beanDefinition.getPropertyValues().add("entityClass", entityType);
            beanDefinition.getPropertyValues().add("mapperInterface", beanClass);
            beanDefinition.setAttribute("factoryBeanObjectType", beanClassName);
            beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            beanDefinition.setLazyInit(this.lazyInitialization);

            if (!scopedProxy) {
                if ("singleton".equals(beanDefinition.getScope()) && this.defaultScope != null) {
                    beanDefinition.setScope(this.defaultScope);
                }
                if (!beanDefinition.isSingleton()) {
                    BeanDefinitionHolder beanDefinitionHolder = new BeanDefinitionHolder(beanDefinition, beanClassName);
                    BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(beanDefinitionHolder, registry, true);
                    if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
                        registry.removeBeanDefinition(proxyHolder.getBeanName());
                    }
                    registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
                }
            }
            if (!registry.containsBeanDefinition(beanClassName)) {
                registry.registerBeanDefinition(beanClassName, beanDefinition);
            }
            registry.removeBeanDefinition(holder.getBeanName());
        }
    }

    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }

    protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
        if (super.checkCandidate(beanName, beanDefinition)) {
            return true;
        } else {
            log.warn("Skipping MapperFactoryBean with name '" + beanName + "' and '" + beanDefinition.getBeanClassName() + "' mapperInterface. Bean already defined with the same name!");
            return false;
        }
    }
}
